# useState

import { ref } from '@vue/reactivity'

const useState = function (initialState) {
  const state = ref(initialState)
  const setState = (value) => {
    // console.log(value)
    state.value = value
  }
  return [state, setState]
}
export default useState
1
2
3
4
5
6
7
8
9
10
11

# defineOptions自定义组件名

defineOptions({
  name: 'MyIndex'
})
1
2
3

# defineProps

defineProps解构丢失响应式的问题

解决办法:

const props = defineProps<Props>();
console.log(props.id)
1
2

从Vue3.5开始,从 defineProps 返回值解构出的变量是响应式的 => 详见官方文档 (opens new window)

# withDefaults

withDefaults(props: defineProps, defaults: Record<string, any>)
1

# defineEmits

使用 defineEmits 函数,你可以明确声明组件可以触发的事件

<script setup lang="ts">
import { defineEmits } from 'vue';

// 使用 defineEmits 来定义组件可以触发的自定义事件
const emit = defineEmits(['update:first', 'update:second']);

// 在组件的逻辑中触发自定义事件
const handleClick = () => {
  emit('update:first', '这是 update:first 事件的数据');
  emit('update:second', '这是 update:second 事件的数据');
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12

类型声明

type TestEvent = {
    'update:info': [val?: any]
    (e: 'update:info', val?: any): any
}
1
2
3
4

# defineModel

我们知道v-model是 :value和@input的语法糖

<input type="text" v-model="localeId" />
<input type="text" :value="localeId" @input="localeId = $event.target.value" />
1
2

上面两段代码是等效的

而defineModel也是个语法糖

defineModel 是对 props 和 emit 的封装,等价于以下代码:

<template>
	<input type="text" :value="modelValue" @input="emit('update:modelValue', $event.target.value)" />
</template>

<script setup>
defineProps({
  modelValue: {
    type: String,
    default: 'Default Value',
  },
});

const emit = defineEmits(['update:modelValue']);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

亦或是

<template>
<p>id <input type="text" v-model="id2" /></p>
</template>

<script setup lang="ts">
defineProps({
  modelValue: {
    type: String,
    default: 'Default Value',
  },
});

const emit = defineEmits(['update:modelValue']);
const localValue = ref(props[modelValue]); // 使用响应式变量绑定父组件数据
watch(localValue, (newValue) => {
    emit(`update:modelValue`, newValue); // 子组件更新 -> 父组件同步
  });
    
// 这里也可以用computed
const localeId = computed({
    get() {
        return props.id
    },
    set(val) {
        emit('update:id', val)
    }
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

编写一个defineModel函数

function defineModel(name = 'modelValue') {
  const props = defineProps({ [name]: Object });
  const emit = defineEmits([`update:${name}`]);

  const localValue = ref(props[name]); // 使用响应式变量绑定父组件数据

  watch(() => props[name], (newValue) => {
    localValue.value = newValue; // 父组件更新 -> 子组件同步
  });

  watch(localValue, (newValue) => {
    emit(`update:${name}`, newValue); // 子组件更新 -> 父组件同步
  });

  return localValue;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

defineModel接收两个参数

第一个参数是绑定的模型名称,第二个参数是配置对象

defineModel返回的是一个Ref对象CustomRefImpl

set 函数必须返回一个值。这个返回值将是 isDeepThink 的最终新值,并且也会是发出 update:isDeepThink 事件时传递给父组件的值。

const id = defineModel('id', {
    type: Number,
    // default: 100,
    required: true,
    set(v) {
        return v
    }
})
1
2
3
4
5
6
7
8

需要注意的是:default和set不能同时设置

# defineModel的应用

defineModel特别适用于ElDialog等组件的visible绑定

# v-memo

https://cn.vuejs.org/api/built-in-directives.html#v-memo

类似于useMemo,缓存一个组件,当依赖不变时,组件内部将不会更新。

<div v-memo="[valueA, valueB]">
  ...
</div>
1
2
3

搭配 v-for 使用 v-memo,确保两者都绑定在同一个元素上。v-memo 不能用在 v-for 内部。

# h渲染函数

Vue3将$createElement从this上移除了,改为使用h

<script setup lang="ts">
import { h } from 'vue'

const renderFunc = () => h('div', 'xxx')
</script>

<template>
  <component :is="renderFunc"></component>
</template>
1
2
3
4
5
6
7
8
9

使用h自定义组件

<template>
  <RenderTask></RenderTask>
</template>

<script setup lang="ts">
const RenderTask = () => h('div', {
  onClick: () => {
    console.log(123);
  }
},'123');
</script>
1
2
3
4
5
6
7
8
9
10
11

# shallowRef

当ref的参数是非常大且具有深层次嵌套的数组对象时,可能会导致性能问题。

使用 shallowRef: 如果您有一个数组对象,而不需要追踪对象属性的变化,可以使用 shallowRef 而不是 ref。shallowRef 只会追踪对象本身的变化,而不会深度追踪嵌套对象的属性。

# shallowReactive

shallowReactive 只追踪对象或数组的顶层属性变化,不会递归处理内部嵌套的对象属性,因此对于数组来说,它会对增删操作进行响应式处理,但不会对数组中对象的属性变化产生响应。

import { shallowReactive, watch } from 'vue';

// 使用 shallowReactive 创建一个浅层响应式数组
const data = shallowReactive([{ id: 1 }, { id: 2 }]);

// 监听数组的长度变化
watch(
  () => data.length, // 监听数组的长度属性
  (newLength, oldLength) => {
    console.log('Array length changed:', newLength, oldLength);
  }
);

// 增加一条数据
data.push({ id: 3 }); // 触发 watch,因为数组长度发生了变化

// 删除一条数据
data.pop(); // 触发 watch,因为数组长度发生了变化

// 修改对象内部属性
data[0].id = 99; // 不会触发 watch,因为 shallowReactive 不递归监听内部属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# shallowReactive和shallowRef

基本定义:

  • shallowReactive:创建一个浅层响应式对象,对象的顶层属性是响应式的,但不会递归地处理内部嵌套的属性。
  • shallowRef:创建一个浅层的 ref,仅对值本身进行响应式处理,而不会递归处理嵌套对象的内部属性。

使用场景:

  • shallowReactive:用于需要响应顶层属性变化的对象或数组场景,尤其是当对象或数组内部有大量嵌套数据时,可以避免不必要的深层追踪。例如,监听对象的增删、数组的增删,而忽略内部属性。
  • shallowRef:适用于只需要响应整体值变化的场景,例如,完全替换对象或数组,而不需要对内部属性的变化产生反应。

# watch

侦听一个 getter 函数:

const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)
1
2
3
4
5
6
7

侦听一个 ref:

const count = ref(0)
watch(count, (newVal, oldVal) => {
  /* ... */
}, {
  deep: true,
  immediate: true
})
1
2
3
4
5
6
7

侦听一组数据

const count = ref<number>(100);
const count1 = ref<number>(200);

watch([count, count1], (newVal, oldVal) => {
  console.log('newVal: ', newVal, oldVal);
});
1
2
3
4
5
6

# watchEffect (opens new window)

# watchEffect与watch❓

  • watchEffect 不需要指定监听的属性,他会自动的收集依赖, 只要我们回调中引用到了 响应式的属性, 那么当这些属性变更的时候,这个回调都会执行,而 watch 只能监听指定的属性而做出变更(v3开始可以同时指定多个)。

  • 第二点就是 watch 可以获取到新值与旧值(更新前的值),而 watchEffect 是拿不到的。

    watch:{
      someValue(newValue,oldValue){
        // ...
      }
    }
    
    1
    2
    3
    4
    5
  • 第三点是 watchEffect 如果存在的话,在组件初始化的时候就会执行一次用以收集依赖(与computed同理),而后收集到的依赖发生变化,这个回调才会再次执行,而 watch 不需要,因为他一开始就指定了依赖。

# computed

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1
  }
})
1
2
3
4
5
6
7

computed函数的第二个参数 debugOptions 包含俩个回调函数

  • onTrack 将在响应属性或引用作为依赖项被跟踪时被调用。
  • onTrigger 将在侦听器回调被依赖项的变更触发时被调用。

# toRef和toRefs

当对一个reactive对象解构时,其内的属性会丢失响应性(如果属性是一个对象,则不会丢失响应性,除非继续解构)

const state = reactive({
  count: 1,
  message: 'Hello',
  obj: {
    count: 100
  }
});

const {
  count,
  obj: { count: count1 }
} = state;
1
2
3
4
5
6
7
8
9
10
11
12

这个时候,使用toRefs包裹reactive对象,则解构出来的属性不会丢失响应性,解构出来的每个属性都是一个ref

const { count } = toRefs(state); // count: Ref<number>
1

而toRef则是针对某一个属性解构

const count = toRef(state, 'count');
1

# effectScope

effectScope 的作用是创建一个副作用作用域,用于隔离和管理副作用代码

const scope = effectScope();

scope.run(() => {
  onMounted(() => {
    console.log('mounted');
  });
  onBeforeUnmount(() => {
    console.log('beforeunmount');
  });
});
1
2
3
4
5
6
7
8
9
10

# provide/inject

provide('globalCount', 1000);

const globalData = inject('globalCount') // 1000
1
2
3

provide/inject的封装使用

这里将store设置为只读类型,以方便控制全局状态的修改

import { inject, provide, reactive, readonly } from 'vue'

type StoreType = {
    name: string
    age: number
}

type ContextType = {
    state: Readonly<StoreType>
    incrementAge: () => void
}

export function ProvideContext() {
    const __state = reactive<StoreType>({
        name: 'zs',
        age: 20
    })
    const incrementAge = () => {
        __state.age++
    }
    provide('GlobalContext', { state: readonly(__state), incrementAge })
}

export function useContext() {
    const context = inject<ContextType>('GlobalContext')
    if (!context) {
        throw new Error('useContext must be used within a ProvideContext!')
    }
    return context
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

使用:

// 父级组件比如App.vue
ProvideContext()

// 子组件
const globalContext = useContext()
1
2
3
4
5

provide('trainType', computed(() => props.trainType)) 和 provide('trainType', props.trainType) 的区别

写法 是否响应 props.trainType 的变化
provide('trainType', props.trainType) ❌ 不会变,值被“拍扁成普通值”
provide('trainType', computed(() => props.trainType)) ✅ 会跟随 props.trainType 变化

一个是响应式一个是普通值

# Vue3中的context使用

在使用inject接收响应式变量时,不能够解构,否则会丢失响应式

JS解构的本质是用新变量接收对象的值,并没有保留原始数据的引用,所以会丢失响应式

const context = reactive({
    name: '',
    age: 20
})

const {name,age} = useFormContext() // 不能解构,会丢失响应式
1
2
3
4
5
6

如果provide提供的是多个响应式变量的集合,那么此时可以使用解构,因为解构出来的还是响应式对象

const formModel = reactive({})
const trainParams = ref([])

const {formModel,trainParams} = useFormContext()
1
2
3
4

# Plugin

插件可以是一个带 install() 方法的对象,亦或直接是一个将被用作 install() 方法的函数。插件选项 (app.use() 的第二个参数) 将会传递给插件的 install() 方法。

import { inject, type App, type ObjectPlugin } from 'vue'

const myPlugin: ObjectPlugin<{ message: string }> = {
    install(app: App, options) {
        console.log('app: ', app)
        // 不推荐
        app.config.globalProperties.$myPluginMethod = () => {
            console.log('myPluginMethod')
        }
        // 推荐
        const myPluginMethod = () => {
            console.log('myPluginMethod', options.message)
        }
        app.provide('myPluginMethod', myPluginMethod)
    }
}

export function useMyPlugin() {
    const myPluginMethod = inject<() => void>('myPluginMethod')
    if (!myPluginMethod) {
        throw new Error('myPlugin is not provided. Make sure to use app.use(myPlugin)!')
    }
    return { myPluginMethod }
}

export default myPlugin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

使用

// main.ts
app.use(myPlugin, { message: 'hello' })

// Component
const { myPluginMethod } = useMyPlugin()
1
2
3
4
5

函数式写法:

export const ourPlugin = (options: { message: string }) => {
    return {
        install(app: App) {
            console.log('app: ', app)
            const ourPluginMethod = () => {
                console.log('ourPluginMethod', options.message)
            }
            app.provide('ourPluginMethod', ourPluginMethod)
        }
    }
}
export function useOurPlugin() {
    const ourPluginMethod = inject<() => void>('ourPluginMethod')
    if (!ourPluginMethod) {
        throw new Error('ourPlugin is not provided. Make sure to use app.use(ourPlugin)!')
    }
    return { ourPluginMethod }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

使用

// main.ts
app.use(ourPlugin({ message: 'hello' }))
1
2

# TypeScript 对 Vue 插件类型推断的一点小坑

在定义插件时,发现类型提示总是失效

/**
 * Vue3 埋点插件
 */

export const TrackerPlugin = {
  install(app: App, config: TrackerConfig) {
    // 创建 Tracker 实例
    const tracker = createTracker(config)

    // 注入全局属性
    app.config.globalProperties.$tracker = tracker

    // 提供依赖注入
    app.provide('tracker', tracker)

    // 自动追踪路由变化(如果使用了 Vue Router)
    if (config.enabled !== false) {
      const router = app.config.globalProperties.$router
      if (router) {
        router.afterEach((to: any) => {
          tracker.track('page_view', {
            path: to.path,
            name: to.name,
            query: to.query
          })
        })
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

main.ts使用

app.use(TrackerPlugin, {
        enabled: true,
        reportUrl: '',
  // ...
    })
1
2
3
4
5

每次输完reportUrl后,后面的属性类型提示就失效了

经查询原来是:

TypeScript 对 Plugin 泛型的推断在 app.use 调用时并不会把它和第二个参数的类型完全绑定,所以在 main.ts 写 app.use(TrackerPlugin, {...}) 时,TS 无法推断 config 的类型,导致写完 reportUrl 后,类型提示就消失了。

解决方案是尽量把插件写成返回 Plugin 的工厂函数

export function createTrackerPlugin(config: TrackerConfig): Plugin {
  return {
    install(app: App) {
      const tracker = createTracker(config)
      app.config.globalProperties.$tracker = tracker
      app.provide('tracker', tracker)

      if (config.enabled !== false) {
        const router = app.config.globalProperties.$router
        if (router) {
          router.afterEach((to: any) => {
            tracker.track('page_view', {
              path: to.path,
              name: to.name,
              query: to.query,
            })
          })
        }
      }
    },
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

总结:Vue3 插件类型推断对 Plugin<T> 在 app.use 中有限支持,用插件工厂函数(方案 1)是目前推荐的做法。

# Directive自定义指令

https://cn.vuejs.org/guide/reusability/custom-directives.html#introduction

<script setup>
const vColor: Directive<any, string> = {
  beforeMount: (el: HTMLDivElement, binding, vnode, prevVNode) => {
    console.log('v-color', el, binding)
    el.style.color = binding.value
  }
}
</script>
1
2
3
4
5
6
7
8
export type Directive<T = any, V = any> = ObjectDirective<T, V> | FunctionDirective<T, V>;
export type DirectiveHook<T = any, Prev = VNode<any, T> | null, V = any> = (el: T, binding: DirectiveBinding<V>, vnode: VNode<any, T>, prevVNode: Prev) => void;
1
2

Directive泛型有两个参数,第一个是el的类型,即dom类型;第二个是自定义指令参数的类型,即binding.value的类型

全局自定义指令

// ObjectDirective
app.directive('bgcolor', {
  beforeMount: (el: HTMLDivElement, binding) => {
    el.style.backgroundColor = binding.value;
  }
});
// FunctionDirective
app.directive('bg-color', (el: HTMLDivElement, binding, vnode, prevVNode) => {
  el.style.backgroundColor = binding.value;
});
1
2
3
4
5
6
7
8
9
10

# 编写的自定义指令如何添加编辑器提示

新建types/directive.d.ts文件,声明合并

import type { FunctionDirective, ObjectDirective } from 'vue'

declare module 'vue' {
    interface ComponentCustomProperties {
        vBgColor: ObjectDirective<any, string>
        vColor: FunctionDirective<any, string>
    }
}
1
2
3
4
5
6
7
8

# Suspense

<script setup lang="ts">
const AsyncComponent = defineAsyncComponent(() =>
    loadWithDelay(import('./pages/AsyncComponent.vue'), 2000)
)
const showAsyncComponent = ref<boolean>(false)
</script>

<template>
	<button @click="showAsyncComponent = !showAsyncComponent">show</button>
    <Suspense>
        <template #fallback>Loading...</template>
        <template #default>
            <async-component></async-component>
        </template>
    </Suspense>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上面的代码中,每次进入页面都会延迟加载2s再显示AsyncComponent组件,这是不符合我们预期的,我们希望的是除第一次进入页面加载延迟外后续能够立刻加载AsyncComponent,就像React那样。

为什么与预期结果不符呢?

这是因为React 的 Suspense 与 Vue 的异步组件加载机制不同

在 React 中,React.lazy 和 Suspense 会利用 React 的内部缓存机制,确保组件加载一次后,下次直接复用已经加载过的模块,避免了每次都重新加载异步组件。换句话说,React 的 Suspense 会确保异步组件在第一次加载后保持在内存中,之后的渲染直接使用已经加载的模块。

而Vue 的 Suspense 没有内建的缓存机制,每次渲染时都会触发异步组件的加载,除非你显式地进行缓存管理。

好了,那么我们要做的事情就很明确了,就是缓存已加载的异步组件。

这里是通过loadWithDelay模拟延迟的,来看代码

export const loadWithDelay = (promise: Promise<any>, time: number) => {
    const delay = (d: number) => new Promise((resolve) => setTimeout(resolve, d))
    const delayPromise = delay(time)
    return Promise.all([promise, delayPromise]).then(() => promise)
}
1
2
3
4
5

添加缓存逻辑:

const cachedAsyncComponent = new Map()

export const loadWithDelay = (promise: Promise<any>, time: number, key: string) => {
    if (cachedAsyncComponent.has(key)) {
        return cachedAsyncComponent.get(key)
    }
    const delay = (d: number) => new Promise((resolve) => setTimeout(resolve, d))
    const delayPromise = delay(time)
    cachedAsyncComponent.set(key, promise)
    return Promise.all([promise, delayPromise]).then(() => promise)
}
1
2
3
4
5
6
7
8
9
10
11

# defineAsyncComponent定义异步组件

在vue2中可以通过

<script>
const AsyncComponent = () =>
  import(/* webpackChunkName: "AsyncComponent" */ '../components/AsyncComponent.vue')
</script>

<template>
  <button @click="asyncComponentShow = true">加载异步组件</button>
  <async-component v-if="asyncComponentShow"></async-component>
</template>
1
2
3
4
5
6
7
8
9

来导入一个异步组件,并在需要时使用

而在Vue3中,导入异步组件需要使用defineAsyncComponent函数

const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'));
1

并且在使用时,外部需要包裹Suspense

<template>
	<div>
    <Suspense>
      <AsyncComponent />
      <template #fallback>
        <div>Loading...</div>
      </template>
    </Suspense>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10

# 使用defineAsyncComponent定义的组件没有编辑器类型提示

解决方案:显式地声明类型

const PDFViewer: DefineComponent<{ fileUrl: string }> = defineAsyncComponent(() =>
    loadWithDelay(import('./PDFViewer.vue'), 2000, 'PDFViewer')
)

// @ts-ignore
const AsyncComponent: DefineSetupFnComponent<AsyncComponentProps, AsyncComponentEmits> = defineAsyncComponent(
  () => loadWithDelay(import('../components/AsyncComponent.vue'), 2000,        'AsyncComponent')
)

const Await: (typeof import('../components/Await.vue'))['default'] = defineAsyncComponent(
    () => import('../components/Await.vue')
)

1
2
3
4
5
6
7
8
9
10
11
12
13

# 动态加载异步组件没有显示fallback内容

<template>
    <p>
        <button @click="showAsyncComponent = !showAsyncComponent">showAsyncComponent</button>
        <Suspense>
            <template #fallback>Loading number...</template>
            <async-component v-if="showAsyncComponent" :id="123"></async-component>
        </Suspense>
    </p>
</template>

<script setup lang="ts" async>
import { defineAsyncComponent, ref } from 'vue'
import { loadWithDelay } from '@/router'
defineOptions({ name: 'AboutView' })
// 在点击时加载组件
const AsyncComponent = defineAsyncComponent(() =>
    loadWithDelay(import('@/components/AsyncComponent.vue'), 2000, 'AsyncComponent')
)
const showAsyncComponent = ref<boolean>(false)
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

正确的修改:

<Suspense v-if="showAsyncComponent">
    <template #fallback>Loading number...</template>
    <async-component :id="123"></async-component>
</Suspense>
1
2
3
4

# Suspense和defineAsyncComponent异步加载的组件优化

<button @click="showAsyncComponent = !showAsyncComponent">showAsyncComponent</button>
<Suspense v-if="showAsyncComponent">
    <template #fallback>Loading number...</template>
    <async-component
                     :number="number"
                     @loaded="isLoaded = true"
                     >
    </async-component>
</Suspense>
1
2
3
4
5
6
7
8
9

上面的代码,点击按钮控制异步组件的显示和隐藏,由于用v-if控制Suspense,每次点击后都会导致asyc-component组件重新加载。

如何在第一次加载异步组件后,后续的显示与隐藏不再重新加载

  1. async-component用v-show控制
  2. Suspense在v-if只执行一次
<Suspense v-if="isLoaded || showAsyncComponent">
            <template #fallback>Loading number...</template>
            <async-component
                :number="number"
                v-show="showAsyncComponent"
                @loaded="isLoaded = true"
            ></async-component>
        </Suspense>
1
2
3
4
5
6
7
8

前面讲到通过异步组件@loaded="isLoaded = true"来减少re-render,看官方文档SuspenseProps后发现其实还可以通过onResolve事件来实现这一效果

<Suspense
          @resolve="isLoaded = true"
          v-if="isLoaded || showAsyncComponent">
    <template #fallback>Loading number...</template>
    <async-component
                     v-show="showAsyncComponent"
                     :number="number"
                     >
    </async-component>
</Suspense>
1
2
3
4
5
6
7
8
9
10

这里需要注意的一个点是async- component组件内必须有一个根节点,这样v-show才能作用在根节点(v-show 的机制是通过操作根节点的 CSS 样式(display: none)来控制组件的显示和隐藏。)

# React lazy 对比 Vue defineAsyncComponent

相比 React lazy,Vue 3 的 defineAsyncComponent 提供更多控制:

const AsyncComponent = defineAsyncComponent({
  loader: () => import("./AsyncComponent.vue"),
  loadingComponent: LoadingComponent, // 加载时显示的组件
  errorComponent: ErrorComponent, // 发生错误时显示的组件
  delay: 200, // 200ms 后才显示 loading
  timeout: 3000, // 3秒后如果还未加载则报错
  onError(error, retry, fail, attempts) {
    if (attempts < 3) {
      retry(); // 失败时重试3次
    } else {
      fail(); // 彻底失败
    }
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Teleport

Vue的Teleport和React的createProtal功能类似,可以把dom结构插入到指定的位置

# attrs、useAttrs

Attributes 继承

“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props (opens new window) 或 emits (opens new window) 的 attribute 或者 v-on 事件监听器。

非props、emits属性会被自动继承到组件的根元素

<span :="{ ...$attrs }">
<span v-bind="{ ...$attrs }">
<span :="{ ...attrs }">
    
<script setup lang="ts">
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
1
2
3
4
5
6
7
8

{ ...$attrs }和React的 { ...props }是相同的作用。

# LRU 缓存策略

class LRUCache {
  private maxSize: number;
  private cache: Map<string, any>;

  constructor(maxSize: number) {
    this.maxSize = maxSize;
    this.cache = new Map();
  }

  set(key: string, value: any) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else if (this.cache.size === this.maxSize) {
      // 删除最不常用的项
      this.cache.delete(this.cache.keys().next().value);
    }
    this.cache.set(key, value);
  }

  get(key: string) {
    if (!this.cache.has(key)) {
      return undefined;
    }
    // 将访问过的项移到末尾,表示它是最近使用的
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);
    return value;
  }
}

const lruCache = new LRUCache(10); // 设置最大缓存大小为10

// 设置缓存
lruCache.set('PDFViewer', componentPromise);

// 获取缓存
const cachedComponent = lruCache.get('PDFViewer');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 动态绑定多个值

let addForm = reactive<AddFormType>({
    name: '',
    age: 0
})

<TestComponent v-bind="addForm" />
1
2
3
4
5
6

在子组件内怎么接收并使用呢

type TestProps = {
    info: AddFormType
    name: string
    age: number
}
const props = defineProps<TestProps>()
1
2
3
4
5
6

注意⚠️:一定要通过类型声明来显式接收变量后方可使用对应props

# 通过props传递组件和通过插槽传递组件

props传递组件:

<TestComponent
	:default="AsyncComponent"
	:info="addForm"
	@update:info="updateInfo"
	v-bind="addForm"
	v-on="getData"
	>
    <template v-slot:default>
        <div>slot default</div>
    </template>
</TestComponent>
1
2
3
4
5
6
7
8
9
10
11

对比React(通过props和props.children)

<TestComponent component={<AsyncComponent />}>
    <AsyncComponent />
</TestComponent>

export default function TestComponent({ component }: TestComponentProps) {
    return (
        <>
            <h3>TestComponent</h3>
            {component}
        </>
    )
}
1
2
3
4
5
6
7
8
9
10
11
12

子组件

<div>
	<component :is="default" v-if="info.age > 25" />
</div>

<script setup lang="ts">
type TestProps = {
    default: Component
}
</script>
1
2
3
4
5
6
7
8
9

# 将props传递给通过RouterView显示的组件

使用 v-slot 来实现

<RouterView v-slot="{ Component }">
    <component :is="Component" :content="content"></component>
</RouterView>
1
2
3

# defineSlots

defineSlots<{
    default(props: { message: string }): any
}>()
1
2
3
<TestComponent
	:default="AsyncComponent"
	:info="addForm"
	@update:info="updateInfo"
	v-bind="addForm"
	v-on="getData"
	>
    <template v-slot:default="slotProps">
        <p>Here is a message from parent: {{ slotProps.message }}</p>
        <AsyncComponent :number="199" />
    </template>
</TestComponent>
1
2
3
4
5
6
7
8
9
10
11
12

slotProps是插槽传递的数据

类型声明

defineSlots<{
    default(props: { message: string }): any
}>()
1
2
3

# 通过app.component全局注册的组件没有编辑器提示

全局注册的组件,由于没有在文件内import,导致lsp服务无法分析组件,故而没有编辑器提示

需要手动扩展Vue 的 GlobalComponents 类型

在项目的 src 目录下创建一个新的文件(通常命名为 global-components.d.ts 或类似名称),用于声明全局组件类型。

declare module '@vue/runtime-core' {
    interface GlobalComponents {
        AsComponent: typeof import('../components/AsyncComponent.vue')['default']
    }
}
1
2
3
4
5

# 符号链接导致的路径问题

但是有个问题,当我们的项目是由pnpm创建的时候,node_modules下没有@vue/runtime-core这个目录

取而代之的是.pnpm目录下的符号链接

解决办法:

使用 TypeScript Paths 显式声明路径

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@vue/runtime-core": [
        "./node_modules/.pnpm/@vue+runtime-core@3.5.3/node_modules/@vue/runtime-core/dist/runtime-core.d.ts"
      ]
    }
  }
}
1
2
3
4
5
6
7
8
9
10

# 动态样式

<span
      v-for="(item, index) in content"
      :class="{
              [$style.active]:
              audioCurrentTime &&
              audioCurrentTime >= item.time[0] &&
              audioCurrentTime <= item.time[1],
              [$style.vad]: true
              }"
      :key="index"
      :data-wb="item.time[0]"
      :data-we="item.time[1]"
      >{{ item.text }}
</span>

<style lang="scss" module>
.active {
    color: #fff;
    background-color: #4499ff;
}
.vad {
    transition:
        background-color 0.05s ease,
        color 0.05s ease;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

这里的[$style.active]是因为使用了cssModule

如果不使用cssModule,而是scoped

<span 
      :class="{
              'audio-item cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap': true,
              'is-active': homeContext.state.currActiveAudioId === item.id
              }"
      @click="chooseAudioRecord(item.id)"
      >{{ item.audioName }}
</span>
1
2
3
4
5
6
7
8

还可以使用数组形式

<span
      :class="[
              'audio-item cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap',
              item.status === AudioStatus.FAILED ? 'failed' : ''
              ]"
      @click="chooseAudioRecord(item.id)"
      :title="item.audioName"
      >
    {{ item.audioName }}
</span>Ï
1
2
3
4
5
6
7
8
9
10

# 给一个组件设置class

在 Vue3 中,当你给一个组件设置 class 属性时,这个属性最终会被应用到组件的根元素上。

如果组件只有一个根元素,则class会被自动应用到根元素上;反之如果组件有多个根元素,Vue3 不会自动决定将 class 应用到哪个元素上。你需要在组件中使用v-bind="$attrs"显式指定哪个元素接收这些属性

# v-bind="$attrs"是干什么的

v-bind="$attrs" 是 Vue 3 中实现 “属性透传”(Attributes Inheritance)的终极武器。它能把你传递给父组件的所有属性、事件、Class 和 Style,一键转交给子组件中的某个特定元素。

在 Vue 3 中,$attrs 包含了:

  • 你没有在 props 中声明的属性(如 data-v-xxx, title, name 等)。
  • 你没有在 emits 中声明的事件(如 @click, @input 等,在 $attrs 中表现为 onClick, onInput)。
  • Class 和 Style。

当你写 v-bind="$attrs" 时,就相当于说:“把所有传给我的、我不认识的东西,全部原封不动地交给这个内部元素。”

# 为什么要用它?(核心场景)

假设你在封装一个自己的 MyInput.vue 组件,内部包裹了一个 el-input。

如果你不用 $attrs,你想支持 placeholder、disabled、type、clearable 等几十个属性,你就得在 props 里一个一个写一遍,非常繁琐。

<!-- MultiRootComponent.vue -->
<template>
  <header>标题</header>
  <main v-bind="$attrs">内容</main>
  <footer>页脚</footer>
</template>
1
2
3
4
5
6

# 禁用属性继承

默认情况下,Vue 会尝试把属性挂在组件的根节点上

如果不希望组件根元素继承class属性,可以给组件设置inheritAttrs: false

<script setup lang="ts">
defineOptions({
  inheritAttrs: false
})
</script>
1
2
3
4
5

# v-bind="props"

只包含你在 defineProps 里明确声明的属性。

而v-bind="$attrs"

包含你在 props 和 emits 之外的所有东西。

# useTemplateRef

<button ref="buttonRef" @click="getRef">getRef</button>

<script>
    const templateRef = useTemplateRef('buttonRef')
    const getRef = () => {
        console.log(templateRef.value)
    }
</script>
1
2
3
4
5
6
7
8

# 定义组件ref

const moultiModalQaRef = ref<InstanceType<typeof MoultiModalQa>>()
1

这样定义的moultiModalQaRef使用起来就有类型提示